package update

import (
	"bytes"
	"crypto"
	"crypto/x509"
	"encoding/pem"
	"errors"
	"fmt"
	"io"
	"io/ioutil"
	"os"
	"path/filepath"

	"gitee.com/go_888/update/internal/go-update/internal/osext"
)

var (
	openFile = os.OpenFile
)

// Apply 方法会使用给定的 io.Reader 中的内容更新当前可执行文件（或 opts.TargetFile，如果已设置）。
// Apply 方法执行以下操作以确保安全的跨平台更新：
// 1. 如果已配置，将更新 io.Reader 的内容作为二进制补丁应用。
// 2. 如果已配置，计算新可执行文件的校验和并验证其是否匹配。
// 3. 如果已配置，使用公钥验证签名。
// 4. 创建一个新文件，路径为 /path/to/.target.new，使用 TargetMode 并包含更新后文件的内容。
// 5. 将 /path/to/target 重命名为 /path/to/.target.old。
// 6. 将 /path/to/.target.new 重命名为 /path/to/target。
// 7. 如果最后的重命名成功，则删除 /path/to/.target.old，并返回无错误。在 Windows 上，
// 删除 /path/to/.target.old 总是失败，因此 Apply 方法会隐藏旧文件而不是删除它。
// 8. 如果最后的重命名失败，则尝试通过将 /path/to/.target.old 重命名为 /path/to/target 来回滚。
// 如果回滚操作失败，则文件系统处于不一致状态（在步骤 5 和 6 之间），此时没有新的可执行文件，并且旧的可执行文件也无法移回到原来的位置。在这种情况下，应通知用户这个坏消息，并要求他们手动恢复。应用程序可以通过调用 RollbackError 来确定回滚是否失败，请参阅该函数的文档以获取更多详细信息。
// md5:15b6e361e791b4d9
func Apply(update io.Reader, opts Options) error {
	// validate
	verify := false
	switch {
	case opts.Signature != nil && opts.PublicKey != nil:
		// okay
		verify = true
	case opts.Signature != nil:
		return errors.New("no public key to verify signature with")
	case opts.PublicKey != nil:
		return errors.New("No signature to verify with")
	}

	// set defaults
	if opts.Hash == 0 {
		opts.Hash = crypto.SHA256
	}
	if opts.Verifier == nil {
		opts.Verifier = NewECDSAVerifier()
	}
	if opts.TargetMode == 0 {
		opts.TargetMode = 0755
	}

	// get target path
	var err error
	opts.TargetPath, err = opts.getPath()
	if err != nil {
		return err
	}

	var newBytes []byte
	if opts.Patcher != nil {
		if newBytes, err = opts.applyPatch(update); err != nil {
			return err
		}
	} else {
		// 没有补丁需要应用，继续前进 md5:d661827be37a22e3
		if newBytes, err = ioutil.ReadAll(update); err != nil {
			return err
		}
	}

	// 如果请求了校验和，则进行验证 md5:0cdc173e228d9f40
	if opts.Checksum != nil {
		if err = opts.verifyChecksum(newBytes); err != nil {
			return err
		}
	}

	if verify {
		if err = opts.verifySignature(newBytes); err != nil {
			return err
		}
	}

	// 获取可执行文件所在的目录 md5:ab556fabdbed0ae0
	updateDir := filepath.Dir(opts.TargetPath)
	filename := filepath.Base(opts.TargetPath)

	// 将 newbinary 的内容复制到一个新的可执行文件中 md5:0017c9cb3652c3e6
	newPath := filepath.Join(updateDir, fmt.Sprintf(".%s.new", filename))
	fp, err := openFile(newPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, opts.TargetMode)
	if err != nil {
		return err
	}
	defer fp.Close()

	_, err = io.Copy(fp, bytes.NewReader(newBytes))
	if err != nil {
		return err
	}

	// 如果我们不调用 fp.Close()，Windows 不会让我们移动新的可执行文件
	// 因为文件仍然会被认为“正在使用中”
	// md5:90be463bf6a3be95
	fp.Close()

	// 这是我们将可执行文件移动到的位置，以便我们可以替换为更新后的版本。
	// 如果有其他需要帮助的地方，请告诉我！ md5:8591a11db03f00c6
	oldPath := opts.OldSavePath
	removeOld := opts.OldSavePath == ""
	if removeOld {
		oldPath = filepath.Join(updateDir, fmt.Sprintf(".%s.old", filename))
	}

	// 删除任何已存在的旧执行文件 - 这在Windows上有两个原因：
	// 1. 在更新成功后，由于进程仍在运行，Windows无法移除 .old 文件
	// 2. 如果目标文件已经存在，Windows的重命名操作会失败
	// 删除任何已存在的旧执行文件 - 这在Windows上有两个原因：
	// 1. 在更新成功后，由于进程仍在运行，Windows无法移除 .old 文件
	// 2. 如果目标文件已经存在，Windows的重命名操作会失败
	// md5:757af3342af4d74b
	_ = os.Remove(oldPath)

	// 将现有的可执行文件移动到同一目录下的新文件中 md5:dad662ce5d35b094
	err = os.Rename(opts.TargetPath, oldPath)
	if err != nil {
		return err
	}

	// 将新的可执行文件移动过来成为新的程序 md5:bc25bd00b06a6f00
	err = os.Rename(newPath, opts.TargetPath)

	if err != nil {
		// 移动操作失败
		// 文件系统现在处于不良状态。我们已经成功地将现有的二进制文件移动到了一个新的位置，但是无法将新的二进制文件移动到原来的位置。这意味着原来的可执行二进制文件的位置现在没有文件！
		// 尝试通过将旧的二进制文件恢复到其原始路径来回滚操作。
		// md5:030e1a1131d966c0
		rerr := os.Rename(oldPath, opts.TargetPath)
		if rerr != nil {
			return &rollbackErr{err, rerr}
		}

		return err
	}

	// 移动成功，如果需要的话移除旧的二进制文件 md5:773e45ec562fad1e
	if removeOld {
		errRemove := os.Remove(oldPath)

		// Windows 在删除旧的二进制文件时会有问题，因此选择隐藏它而不是删除。 md5:af7ccfd0dd4af5cb
		if errRemove != nil {
			_ = hideFile(oldPath)
		}
	}

	return nil
}

// RollbackError 接收由 Apply 返回的错误值，并返回在尝试从失败的更新中回滚时发生的任何错误。
// 应用程序应该始终在由 Apply 返回的任何非空错误上调用此函数。
// 如果不需要回滚或回滚成功，则 RollbackError 返回 nil；否则，它会返回尝试回滚时遇到的错误。
// md5:d9a55b60994d3c55
func RollbackError(err error) error {
	if err == nil {
		return nil
	}
	if rerr, ok := err.(*rollbackErr); ok {
		return rerr.rollbackErr
	}
	return nil
}

type rollbackErr struct {
	error             // original error
	rollbackErr error // 回滚时遇到错误 md5:fdef66221068eaf4
}

type Options struct {
	// TargetPath 定义了要更新的文件的路径。
	// 空字符串表示“正在运行的程序的可执行文件”。
	// md5:e3f82681289d85e6
	TargetPath string

	// 使用此文件模式创建 TargetPath 的替换。如果为零，则默认为 0755。 md5:57ccc00935a5a035
	TargetMode os.FileMode

	// 新二进制文件的校验和以进行验证。如果为 nil，则不执行校验和或签名验证。 md5:080f4cdd6122ba5c
	Checksum []byte

	// 用于签名验证的公钥。如果为 nil，则不进行签名验证。 md5:849332a4fb57156f
	PublicKey crypto.PublicKey

	// 用于验证更新后的文件的签名。如果为 `nil`，则不进行签名验证。 md5:e736559843127a50
	Signature []byte

	// 可插拔的签名验证算法。如果为 nil，则使用 ECDSA 算法。 md5:f386039703b445ec
	Verifier Verifier

	// 使用此哈希函数生成校验和。如果未设置，则使用SHA256。 md5:a80742fd59b0e4b8
	Hash crypto.Hash

	// 如果为nil，则将更新视为对TargetPath文件内容的完全替换。
	// 如果非nil，则将更新内容视为补丁，并使用此对象来应用补丁。
	// md5:ccbfcfe7330902a1
	Patcher Patcher

	// 在更新成功后，将旧的可执行文件存储在此路径下。
	// 空字符串意味着更新后将删除旧的可执行文件。
	// md5:81e1d956fd29ec49
	OldSavePath string
}

// CheckPermissions 用于确定进程是否有正确的权限来执行请求的更新。如果可以继续进行更新，则返回 nil，否则返回尝试更新时会发生的错误。
// md5:3cc04479bd60cd5e
func (o *Options) CheckPermissions() error {
	// 获取文件所在的目录 md5:1b75de53a788b039
	path, err := o.getPath()
	if err != nil {
		return err
	}

	fileDir := filepath.Dir(path)
	fileName := filepath.Base(path)

	// 尝试在文件的目录中打开一个文件 md5:c138044fc4878c74
	newPath := filepath.Join(fileDir, fmt.Sprintf(".%s.new", fileName))
	fp, err := openFile(newPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, o.TargetMode)
	if err != nil {
		return err
	}
	fp.Close()

	_ = os.Remove(newPath)
	return nil
}

// SetPublicKeyPEM 是一个便捷方法，用于设置用于检查已完成更新的签名的 PublicKey 属性。
// 该方法通过解析格式化为 PEM 数据的公钥来实现。
// md5:28874e92a6261bbd
func (o *Options) SetPublicKeyPEM(pembytes []byte) error {
	block, _ := pem.Decode(pembytes)
	if block == nil {
		return errors.New("couldn't parse PEM data")
	}

	pub, err := x509.ParsePKIXPublicKey(block.Bytes)
	if err != nil {
		return err
	}
	o.PublicKey = pub
	return nil
}

func (o *Options) getPath() (string, error) {
	if o.TargetPath == "" {
		return osext.Executable()
	} else {
		return o.TargetPath, nil
	}
}

func (o *Options) applyPatch(patch io.Reader) ([]byte, error) {
	// open the file to patch
	old, err := os.Open(o.TargetPath)
	if err != nil {
		return nil, err
	}
	defer old.Close()

	// apply the patch
	var applied bytes.Buffer
	if err = o.Patcher.Patch(old, &applied, patch); err != nil {
		return nil, err
	}

	return applied.Bytes(), nil
}

func (o *Options) verifyChecksum(updated []byte) error {
	checksum, err := checksumFor(o.Hash, updated)
	if err != nil {
		return err
	}

	if !bytes.Equal(o.Checksum, checksum) {
		return fmt.Errorf("Updated file has wrong checksum. Expected: %x, got: %x", o.Checksum, checksum)
	}
	return nil
}

func (o *Options) verifySignature(updated []byte) error {
	checksum, err := checksumFor(o.Hash, updated)
	if err != nil {
		return err
	}
	return o.Verifier.VerifySignature(checksum, o.Signature, o.Hash, o.PublicKey)
}

func checksumFor(h crypto.Hash, payload []byte) ([]byte, error) {
	if !h.Available() {
		return nil, errors.New("requested hash function not available")
	}
	hash := h.New()
	hash.Write(payload) // 确保不会出错 md5:12ecec218022111b
	return hash.Sum([]byte{}), nil
}
